跨源资源共享 CORS 和 Csrf 的概念
跨域资源共享(CORS)
跨域:A域名访问 B域名的数据(域名或者请求端口或者协议不一致的时候就跨域了)
www.example.com www.example.cn 跨域
www.example.com:8080 www.example.com:8081 跨域
www.example.com example.cn 跨域
http://www.example.com https://www.example.cn 跨域
什么是 CORS?
CORS
全称是 Cross-Origin Resource Sharing
,直译过来就是跨域资源共享。要理解这个概念就需要知道 域、资源 和 同源策略 这三个概念。
- 域,指的是一个站点,由
protocol
、host
和port
三部分组成,其中host
可以是域名,也可以是ip
;port
如果没有指明,则是使用protocol
的默认端口 - 资源,是指一个 URL 对应的内容,可以是一张图片、一种字体、一段 HTML 代码、一份 JSON 数据等等任何形式的任何内容
- 同源策略,指的是为了防止
XSS
,浏览器、客户端应该仅请求与当前页面来自同一个域的资源,请求其他域的资源需要通过验证。
了解了这三个概念,我们就能理解为什么有 CORS 规范了:从站点 A 请求站点 B 的资源的时候,由于浏览器的同源策略的影响,这样的跨域请求将被禁止发送;为了让跨域请求能够正常发送,我们需要一套机制在不破坏同源策略的安全性的情况下、允许跨域请求正常发送,这样的机制就是 CORS
。
如下,就是没有同源策略导致的
当用户登录受信任网站 A 时,A 会在本地生成 Cookie,这时用户如果在不登出 A 的情况下,访问危险网站 B,B 就可以拿到 A 给用户的 Cookie 再给 A 发请求
什么是 Csrf?
CSRF,跨站请求伪造。攻击者在受害者未知的情况下可以使用受害者的认证发送伪造请求给目标站点。
从字面上看,CSRF 是一种攻击方式。而CORS是一种资源共享的方式。
所以为了防止被 CSRF 攻击,引入了同源策略这个机制,同源策略主要是防止客户端这块被劫持,而非服务端。如果需要跨域访问时就在请求头加上的这个域名的字段列表
出于安全原因,浏览器限制从脚本内发起的跨源 HTTP 请求,XMLHttpRequest 和 Fetch API,只能从加载应用程序的同一个域请求 HTTP 资源,除非使用 CORS 头文件
对于 浏览器限制 这个词,要着重解释一下:不一定是浏览器限制了发起跨站请求,也可能是跨站请求可以正常发起,但是返回结果被浏览器拦截了
注意:跨域不只可以用 CORS 还可以使用 JSONP 的方式,但是 JSONP 只是一种绕过浏览器的方式,有点投机取巧且不安全。所以一般使用 CORS 这个规范(CORS 是一个 W3C 标准,全称是 "跨域资源共享")
同源策略详情
同源策略是一个重要的安全策略,它用于限制一个源的网站或者它加载的脚本如何能与另一个源的资源进行交互。同源政策的目的,是为了保证用户信息的安全,防止恶意的网站窃取数据
设想这样一种情况:A网站是一家银行,用户登录以后,又去浏览其他网站。如果其他网站可以读取A网站的 Cookie,会发生什么?
很显然,如果 Cookie 包含隐私(比如存款总额),这些信息就会泄漏。更可怕的是,Cookie 往往用来保存用户的登录状态,如果用户没有退出登录,其他网站就可以冒充用户,为所欲为。因为浏览器同时还规定,提交表单不受同源政策的限制。
由此可见,"同源政策"是必需的,否则 Cookie 可以共享,互联网就毫无安全可言了。
怎么判别是同源?
如果两个 URL 的 协议、端口 (如果有指定的话)和 域名 都相同的话,则这两个 URL 是同源
即便两个不同的域名指向同一个ip地址,也非同源。
同源策略有什么限制?
- Cookie、LocalStorage、IndexedDB 等存储性内容 例如:A网页设置的 Cookie,B网页不能打开,除非这两个网页"同源"
- DOM 节点 无法获得
- AJAX 请求不能发送
但是有三个标签是允许跨域加载资源:
<img src=XXX>
<link href=XXX>
<script src=XXX>
注:之所以要贴下面这个是因为某些答案一本正经的胡说八道,顶级域 != 顶级域名
顶级域比如 .com .org .cn 但是注意了,顶级域 != 顶级域名
一级域名又称顶级域名 例如:baidu.com
zhihu.com
qq.com
未配置 CORS 的例子
页面端:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<h1>返回测试页面</h1>
<script>
fetch("http://localhost:3001/")
.then(res => {
return res.text();
})
.then(res => {
alert(res);
});
</script>
</body>
</html>
服务器端:
const express = require('express');
// 3000端口的服务 将当前目录作为http服务
let app = express();
// 注意,当静态目录下有 index.html 默认将这个html返回出去
app.use(express.static(__dirname))
app.listen(3000)
// 3001端口的服务 返回数据
let app2 = express();
app2.get('/', (rep, res) => {
res.send("你好")
})
app2.listen(3001)
出现如下报错
JSONP 和 CORS
上面的原因就是跨域错误,一般主流的解决办法有两个
1、 JSONP:全称 JSON with Padding
JSONP 是一种 非正式传输协议(利用早期制定同源策略的 “漏洞”),该协议的一个要点就是允许用户传递一个 callback 参数给服务端,然后服务端返回数据时会将这个 callback 参数作为函数名来包裹住 JSON 数据,这样客户端就可以随意定制自己的函数来自动处理返回数据了。
原理:就是利用 <script>
标签没有跨域限制的 “漏洞” 来达到与第三方通讯的目的,用法就是客户端传过去一个函数名,服务端收到这个函数名再和数据进行拼接形成一个函数调用
2、 CORS: 全称 Cross-Origin Resource Sharing
即:跨来源资源共享。它是一份浏览器技术的规范,提供了 Web 服务从不同网域传来沙盒脚本的方法,以避开浏览器的同源策略,JSONP模式的现代版
具体原理看下面
简单来说,就是浏览器匹配请求头和响应头,如果符合要求便可拿到数据,否则无法拿到数据。整个过程都是由浏览器自动完成。这就像一个白名单,代表着谁可以拿到数据。
服务端是如何支持的?
跨域资源共享标准新增了一组 HTTP 首部字段,允许服务器声明哪些源站通过浏览器有权限访问哪些资源。
另外,规范要求,对那些可能对服务器数据产生副作用的 HTTP 请求方法(特别是 GET 以外的 HTTP 请求,或者搭配某些 MIME 类型的 POST 请求),浏览器必须首先使用 OPTIONS 方法发起一个预检请求(preflight request),从而获知服务端是否允许该跨域请求。
服务器确认允许之后,才发起实际的 HTTP 请求。在预检请求的返回中,服务器端也可以通知客户端,是否需要携带身份凭证(包括 Cookies 和 HTTP 认证相关数据)。
所以 CORS请求分成两类:简单请求(simple request)和非简单请求(not-so-simple request)
简单请求
不会触发 CORS
预检的请求称为简单请求,满足以下 所有条件 的才会被视为简单请求,基本上我们日常开发只会关注前面两点
使用 GET
、POST
、HEAD
其中一种方法
只使用了如下的安全首部字段,不得人为设置其他首部字段
Accept
Accept-Language
Content-Language
Content-Type
仅限以下三种text/plain
multipart/form-data
application/x-www-form-urlencoded
- HTML 头部
header field
字段:DPR、Download、Save-Data、Viewport-Width、WIdth
请求中的任意XMLHttpRequestUpload
对象均没有注册任何事件监听器;XMLHttpRequestUpload
对象可以使用XMLHttpRequest.upload
属性访问 - 请求中没有使用
ReadableStream
对象
非简单请求
顾名思义,只要是会触发 CORS
预检的请求就是非简单请求,其在请求前会先发送一个预检请求(preflight request)
需预检的请求要求必须首先使用 OPTIONS 方法发起一个预检请求到服务器,以获知服务器是否允许该实际请求。预检请求的使用,可以避免跨域请求对服务器的用户数据产生未预期的影响
下面的请求会触发预检请求,其实非简单请求之外的就会触发预检,就不用记那么多了
使用了 PUT、DELETE、CONNECT、OPTIONS、TRACE、PATCH
方法
人为设置了非规定内的其他首部字段,参考上面简单请求的安全字段集合,还要特别注意 Content-Type
的类型
XMLHttpRequestUpload
对象注册了任何事件监听器
请求中使用了 ReadableStream
对象
发起一次 OPTIONS 请求,会带上下面三个 headers
:
Origin
:值为当前页面所在的域,用于告诉服务器当前请求的域。如果没有这个header
,服务器将不会进行CORS
验证。Access-Control-Request-Method
:值为实际请求将会使用的方法Access-Control-Request-Headers
:值为实际请求将会使用的header
集合
如果服务器端 CORS
验证失败,则会返回客户端错误,即 4xx 的状态码。
如果请求成功,返回 200 的状态码,并在响应中带上下面这些 headers
:
Access-Control-Allow-Origin
:允许请求的域,多数情况下,就是预检请求中的Origin
的值Access-Control-Allow-Credentials
:一个布尔值,表示服务器是否允许使用cookies
Access-Control-Expose-Headers
:实际请求中可以出现在响应中的headers
集合Access-Control-Max-Age
:预检请求返回的规则可以被缓存的最长时间,超过这个时间,需要再次发起预检请求Access-Control-Allow-Methods
:实际请求中可以使用到的方法集合 浏览器会根据预检请求的响应,来决定是否发起实际请求。
以下是一个发起预检请求的例子 发起请求的 origin 与请求的服务器的 host 不同,而且根据上面的条件判断,触发了预检
请求附带 cookies
如果发起请求时设置 withCredentials
标志设置为 true
,从而向服务器发送 cookie
, 但是如果服务器端的响应中未携带Access-Control-Allow-Credentials: true
,浏览器将不会把响应内容返回给请求的发送者
对于附带身份凭证的请求,服务器不得设置 Access-Control-Allow-Origin
的值为 *
, 必须是某个具体的域名
注意,简单 GET 请求不会被预检;如果对此类带有身份凭证请求的响应中不包含该字段,这个响应将被忽略掉,并且浏览器也不会将相应内容返回给网页
完整的流程
SpringBoot配置
注意:使用了 AllowCredentials
为 true 之后不允许设置 AllowedOrigin
为 *
CorsFilter 全局配置
@Configuration
public class GlobalCorsConfig {
@Bean
public CorsFilter corsFilter() {
CorsConfiguration config = new CorsConfiguration();
//开放哪些ip、端口、域名的访问权限,星号表示开放所有域
// 升级 springboot2.4.0 后, allowedOrigin 不能用通配符 *
config.addAllowedOrigin("http://127.0.0.1:5500");
//是否允许发送Cookie信息
// 注意这个为 true 后下面的 addAllowedHeader 不能为 *
config.setAllowCredentials(false);
//开放哪些Http方法,允许跨域访问 全开用 config.addAllowedMethod("*");
config.addAllowedMethod(HttpMethod.GET);
config.addAllowedMethod(HttpMethod.POST);
config.addAllowedMethod(HttpMethod.PUT);
config.addAllowedMethod(HttpMethod.DELETE);
//允许HTTP请求中的携带哪些Header信息
config.addAllowedHeader("*");
//暴露哪些头部信息(因为跨域访问默认不能获取全部头部信息)
// 这里必须一个个添加(居然不能再用 * 了,淦)
config.addExposedHeader("Content-Type");
config.addExposedHeader( "X-Requested-With");
config.addExposedHeader("accept");
config.addExposedHeader("Origin");
config.addExposedHeader( "Access-Control-Request-Method");
config.addExposedHeader("Access-Control-Request-Headers");
//添加映射路径,“/**”表示对所有的路径实行全局跨域访问权限的设置
UrlBasedCorsConfigurationSource configSource = new UrlBasedCorsConfigurationSource();
configSource.registerCorsConfiguration("/**", config);
return new CorsFilter(configSource);
}
}
addCorsMappings 方法配置
这个也是全局配置,实现 WebMvcConfigurer
接口的 addCorsMappings
方法,本质就是一个拦截器
@Configuration
public class GlobalCorsConfig {
@Bean
public WebMvcConfigurer corsConfigurer() {
return new WebMvcConfigurer() {
@Override
public void addCorsMappings(CorsRegistry registry) {
// // 添加映射路径,“/**”表示对所有的路径实行全局跨域访问权限的设置
registry.addMapping("/**")
// 开放哪些ip、端口、域名的访问权限
// 例如:.allowedOrigins("http://127.0.0.1:5500")(用逗号分隔多个)
// 升级 springboot2.4.0 后, allowedOrigin 不能用通配符 *
.allowedOrigins("http://127.0.0.1:5500")
// 是否允许发送Cookie信息
.allowCredentials(true)
// 开放哪些Http方法,允许跨域访问 * 表示全部
.allowedMethods("GET","POST", "PUT", "DELETE")
// 允许HTTP请求中的携带哪些Header信息
.allowedHeaders("*")
// 暴露哪些头部信息(因为跨域访问默认不能获取全部头部信息)
// 这里必须一个个添加
.exposedHeaders(
"Content-Type",
"X-Requested-With",
"accept",
"Origin",
"Access-Control-Request-Method",
"Access-Control-Allow-Origin",
"Access-Control-Request-Headers");
}
};
}
}
CrossOrigin注解
- 将
@CrossOrigin
注解加在 Controller 层的方法上,该方法定义的 RequestMapping 端点将支持跨域访问 - 将
@CrossOrigin
注解加在 Controller 层的类定义处,整个类所有的方法对应的 RequestMapping 端点都将支持跨域访问
@RequestMapping("/cors")
@ResponseBody
@CrossOrigin(origins = "http://localhost:8080", maxAge = 3600)
public String cors( ){
return "cors";
}
使用原生过滤器
@Component
public class CORSFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletResponse res = (HttpServletResponse) response;
res.addHeader("Access-Control-Allow-Credentials", "true");
res.addHeader("Access-Control-Allow-Origin", "*");
res.addHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, PUT");
res.addHeader("Access-Control-Allow-Headers", "Content-Type,X-CAF-Authorization-Token,sessionToken,X-TOKEN");
if (((HttpServletRequest) request).getMethod().equals("OPTIONS")) {
response.getWriter().println("ok");
return;
}
chain.doFilter(request, response);
}
@Override
public void destroy() {
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
}
配置测试环境
使用 Vue 快速搭建一个异步请求的环境
<div id="app">
<input type="number" v-model="number">
<button @click="commit">提交测试</button>
</div id>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<script>
let vm = new Vue({
el: "#app",
data: {
number: 0
},
methods: {
commit: function () {
let that = this;
axios.get('http://localhost:8080/hello/temp3',{
params: {
id: that.number
}
})
.then(res =>{
console.log(res.data);
})
}
},
})
</script>
Spring Security 配置
在引入了 Spring Security 之后,我们会发现前面的方法都不能正确的配置 CORS,每次 preflight request 都会得到一个 401 的状态码,表示请求没有被授权。这时,我们需要增加一点配置才能让 CORS 正常工作:
下面这些原因,导致了 preflight request
无法通过身份验证,从而导致 CORS
失效:
preflight request
不会携带认证信息- Spring Security 通过
Filter
来进行身份验证 - Interceptor 和
HttpRequestHanlder
在DispatcherServlet
之后被调用 - Spring Security 中的
Filter
优先级比我们注入的CorsFilter
优先级高
Filter 与 Interceptor
在学习这些配置 CORS 之前,先复习一下 Filter
与 Interceptor
的概念。
首先是过滤器和拦截器的执行时间,一个作用在 DispatcherServlet
调用前,一个作用在调用后。
Filter
由 Servlet
标准定义,要求 Filter
需要在 Servlet
被调用之前调用;在 Spring Web 应用中,DispatcherServlet
就是唯一的 Servlet
实现。
Interceptor
由 Spring 自己定义,由 DispatcherServlet
调用,可以定义在 Handler
调用前后的行为。这里的 Handler
,在多数情况下,就是我们的 Controller
中对应的方法。
addCorsMappings 注册原理
WebMvcConfigurer.addCorsMappings
方法做了什么?
我们从 WebMvcConfigurer.addCorsMappings
方法的参数开始,先看看 CORS
配置是如何保存到 Spring 上下文中的,然后在了解一下 Spring 是如何使用的它们。
default void addCorsMappings(CorsRegistry registry) {}
首先就是这个传入的参数 CorsRegistry
,这个参数用于注册 CORS
的配置
public class CorsRegistry {
private final List<CorsRegistration> registrations = new ArrayList<>();
// 接收一个 pathPattern,创建一个 CorsRegistration 实例,保存到列表后将其返回。
// 例如上面的配置,这里的 pathPattern 就是 /**
public CorsRegistration addMapping(String pathPattern) {
CorsRegistration registration = new CorsRegistration(pathPattern);
this.registrations.add(registration);
return registration;
}
// 方法将保存的 CORS 规则转换成 Map 后返回
protected Map<String, CorsConfiguration> getCorsConfigurations() {
Map<String, CorsConfiguration> configs = new LinkedHashMap<>(this.registrations.size());
for (CorsRegistration registration : this.registrations) {
configs.put(registration.getPathPattern(), registration.getCorsConfiguration());
}
return configs;
}
}
继续下一层,这个 addMapping
方法的返回类型 CorsRegistration
(这里只展示部分源码)
public class CorsRegistration {
private final String pathPattern;
private final CorsConfiguration config; // 保存的一个 pathPattern 对应的 CORS 规则
....
public CorsRegistration(String pathPattern) {
this.pathPattern = pathPattern;
/**
* 这个 applyPermitDefaultValues() 方法就是配置默认的 CORS 规则
*
* allowedOrigins 默认为所有域
* allowedMethods 默认为 GET 、HEAD 和 POST
* allowedHeaders 默认为所有
* maxAge 默认为 30 分钟
* exposedHeaders 默认为 null,也就是不暴露任何 header
* credentials 默认为 null
*
* 创建好这个 CorsRegistration 实例后可以通过它的 allowedOrigins、
* allowedMethods 等方法修改它的 CorsConfiguration,覆盖掉上面的
* 默认值
*/
this.config = new CorsConfiguration().applyPermitDefaultValues();
}
public CorsRegistration allowedOrigins(String... origins) {
this.config.setAllowedOrigins(Arrays.asList(origins));
return this;
}
}
现在,我们已经通过 WebMvcConfigurer.addCorsMappings
方法配置好 CorsRegistry
了
如何注册到 Security 里
上面的都是使用 Interceptor 注册 CORES 的原理,如果只是拿了就用,则直接使用 HtttpSecurity.cors()
方法
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter() {
protected void configure(HttpSecurity http) throws Exception {
// 使用这个方法把前面注册的 CorsFilter Bean 加入 Filter
http.cors();
}
}
手动跨域
对于实在用不了全局跨域的方法可以手动跨域
// 在响应头上加上
// 允许跨域
resp.setHeader("Access-Control-Allow-Origin", "*");
// 允许自定义请求头token(允许head跨域)
resp.setHeader("Access-Control-Allow-Headers", "token, Accept, Origin, X-Requested-With, Content-Type, Last-Modified");
@CrossOrigin 跨域
controller 方法的 CORS配置,可以向 @RequestMapping
注解处理程序方法添加一个 @CrossOrigin
注解,以便启用 CORS(默认情况下,@CrossOrigin
允许在 @RequestMapping
注解中指定的所有源和 HTTP 方法):
// 或者直接加在类上全局跨域
@RestController
@RequestMapping("/account")
public class AccountController {
@CrossOrigin
@GetMapping("/{id}")
public Account retrieve(@PathVariable Long id) {
// ...
}
@DeleteMapping("/{id}")
public void remove(@PathVariable Long id) {
// ...
}
}
其中 @CrossOrigin
中的 2个参数:
- origins :允许可访问的域列表
- maxAge:准备响应前的缓存持续的最大时间(以秒为单位)。
为整个 controller 启用 @CrossOrigin
@CrossOrigin(origins = "http://domain2.com", maxAge = 3600)
@RestController
@RequestMapping("/account")
public class AccountController {
@GetMapping("/{id}")
public Account retrieve(@PathVariable Long id) {
// ...
}
@DeleteMapping("/{id}")
public void remove(@PathVariable Long id) {
// ...
}
}
在这个例子中,对于 retrieve()
和 remove()
处理方法都启用了跨域支持,还可以看到如何使用 @CrossOrigin
属性定制 CORS 配置。
全局 CORS 配置
除了细粒度、基于注释的配置之外,还可能需要定义一些全局 CORS 配置。这类似于使用筛选器,但可以声明为 Spring MVC 并结合细粒度 @CrossOrigin
配置。默认情况下,所有 origins and GET, HEAD and POST methods 是允许的。
JavaConfig,使整个应用程序的 CORS 简化为:
@Configuration
@EnableWebMvc
public class WebConfig extends WebMvcConfigurerAdapter {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**");
}
}
如果正在使用 Spring Boot,建议将 WebMvcConfigurer bean 声明如下:
@Configuration
public class MyConfiguration {
@Bean
public WebMvcConfigurer corsConfigurer() {
return new WebMvcConfigurerAdapter() {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**");
}
};
}
}
可以轻松地更改任何属性,以及仅将此 CORS 配置应用到特定的路径模式:
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOrigins("http://domain2.com")
.allowedMethods("PUT", "DELETE")
.allowedHeaders("header1", "header2", "header3")
.exposedHeaders("header1", "header2")
.allowCredentials(false).maxAge(3600);
}
总结
- 注入
CorsFilter
的方式会让CORS
验证在Filter
中生效 - 引入 Spring Security 后,需要调用
HttpSecurity.cors
方法以保证CorsFilter
会在身份验证相关的Filter
之前执行 HttpSecurity.cors
+WebMvcConfigurer.addCorsMappings
是一种相对低效的方式,会导致跨域请求分别在Filter
和Interceptor
层各经历一次CORS
验证HttpSecurity.cors
+ 注册CorsFilter
与HttpSecurity.cors
+ 注册CorsConfigurationSource
在运行的时候是等效的- 在 Spring 中,没有通过
CORS
验证的请求会得到状态码为 403 的响应
Ajax 请求
请求携带 Cookie
默认跨域请求是不携带 cookie 的,所以需要进行设置 这里只讲 axios 的设置方法
axios.interceptors.request.use(config => {
config.withCredentials = true;
return config;
});
这样每次请求就会携带上 Cookie 了(注意,后端也要进行相应的设置,看上面)
无法携带 Cookie的问题
突然发现进行了上面的设置依旧无法使用 Cookie,这是因为浏览器一个叫做 SameSite 的设置搞得鬼(气死我了,弄了半天还以为是我同源策略的问题!!!直到看到了这个新特性才发现不是自己的问题,淦!!)
但是暂时还没有看到很好的解决方案,如果只是测试一些其它工具而不是生产环境下,那可以设置在浏览器的 edge://flags/
暂时关闭这个设置 SameSite by default cookies
改成 Disable
待更新...
References
MDN-跨源资源共享(CORS) 不要再问我跨域的问题了 浅谈 CSRF 攻击方式 Spring 里那么多种 CORS 的配置方式,到底有什么区别 CORS 简单请求+预检请求(彻底理解跨域) 阮一峰-Cookie 的 SameSite 属性 谷歌浏览器新版本Chrome 80默认SameSite导致跨域登录状态失效的问题 浏览器系列之 Cookie 和 SameSite 属性 应对浏览器Cookie新属性sameSite的临门一脚 springboot解决谷歌80及以上版本的SameSite设置cookie失效 Spring Boot中的跨域,为什么加了 Spring Security 就失效了呢? Spring 里那么多种 CORS 的配置方式,到底有什么区别